Skip to content

Conversation

@jshin-relativity
Copy link

@jshin-relativity jshin-relativity commented Feb 11, 2026

In the HangfireWorkflowScheduler, it is possible for the GetRecurringJobIds method to throw a null reference exception, and I have observed this occurring when multiple instances have their triggers being updated after a workflow definition change at the same time, even with distributed locking set up in Elsa. I believe this will at least prevent the null reference exception; without this, the handler crashes and recurring jobs get left around.

Below is a snippet from the Hangfire code that retrieves the list

https://github.com/HangfireIO/Hangfire/blob/eb994c324c0ed2687e2c35412c72460a1c1f9e71/src/Hangfire.Core/Storage/StorageConnectionExtensions.cs#L75

        public static List<RecurringJobDto> GetRecurringJobs([NotNull] this IStorageConnection connection)
        {
            if (connection == null) throw new ArgumentNullException(nameof(connection));

            var ids = connection.GetAllItemsFromSet("recurring-jobs");
            return GetRecurringJobDtos(connection, ids);
        }

And the GetRecurringJobDtos method can definitely return objects without a Job set.

https://github.com/HangfireIO/Hangfire/blob/eb994c324c0ed2687e2c35412c72460a1c1f9e71/src/Hangfire.Core/Storage/StorageConnectionExtensions.cs#L89

        private static List<RecurringJobDto> GetRecurringJobDtos(IStorageConnection connection, IEnumerable<string> ids)
        {
            var result = new List<RecurringJobDto>();
            foreach (var id in ids)
            {
                var hash = connection.GetAllEntriesFromHash($"recurring-job:{id}");

                // TODO: Remove this in 2.0 (breaking change)
                if (hash == null)
                {
                    result.Add(new RecurringJobDto { Id = id, Removed = true });
                    continue;
                }

                var dto = new RecurringJobDto { Id = id };

                if (hash.TryGetValue("Cron", out var cron) && !String.IsNullOrWhiteSpace(cron))
                {
                    dto.Cron = cron;
                }

                try
                {
                    if (hash.TryGetValue("Job", out var payload) && !String.IsNullOrWhiteSpace(payload))
                    {
                        var invocationData = InvocationData.DeserializePayload(payload);
                        dto.Job = invocationData.DeserializeJob();
                    }
                }
                catch (JobLoadException ex)
                {
                    dto.LoadException = ex;
                }

@greptile-apps
Copy link

greptile-apps bot commented Feb 11, 2026

Greptile Overview

Greptile Summary

Added defensive null check to GetRecurringJobIds method to prevent NullReferenceException in race conditions when multiple instances update workflow triggers simultaneously.

  • The pattern check x.Job is { Args.Count: > 0 } ensures both Job and Args are non-null before accessing Args[0]
  • Prevents crashes during concurrent job cleanup when distributed locking doesn't fully prevent race conditions
  • Maintains existing filtering logic while making it more resilient to edge cases

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk
  • The defensive null check is a targeted fix addressing a real production issue without changing behavior for valid cases. The pattern matching syntax correctly validates both Job and Args existence before array access.
  • No files require special attention

Important Files Changed

Filename Overview
src/modules/scheduling/Elsa.Scheduling.Hangfire/Services/HangfireWorkflowScheduler.cs Added null-safety check to prevent NullReferenceException when accessing job arguments in concurrent scenarios

@jshin-relativity
Copy link
Author

@dotnet-policy-service agree company="Relativity Holdings"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants